﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2014-01-20 14:25
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BassDrop.Core.Common.Services;

namespace BassDrop.Core.Desktop.Services
{
	/// <summary>
	/// Desktop implementation of the ICryptoProvider interface providing encryption and decription functions
	/// </summary>
	public class CryptoProvider : ICryptoProvider
	{
		/// <summary>
		/// Symmetrically encrypts the passed string with the passed parameters
		/// and returns a base64 representation of the resulting cyphertext.
		/// </summary>
		/// <param name="plainText">the text to encrypt</param>
		/// <param name="password">the password to use for encryption</param>
		/// <param name="salt">the salt to use for the encryption - if null or left out, no salting is done</param>
		/// <param name="strength">the arbitrary strength of the encryption - usually means the number of iterations</param>
		/// <returns></returns>
		public string Encrypt( string plainText, string password, string salt = null, CryptoStrength strength = CryptoStrength.Normal )
		{
			return SimpleEncryptWithPassword( plainText, password ); // TODO: salt és cryptostrength felhasználása
		}

		/// <summary>
		/// Decrypts the passed cyphertext using the passed parameters
		/// (as encrypted by the Encrypt method - symmetrically with the passed parameters and encoded in base64)
		/// and returns the resulting plaintext.
		/// </summary>
		/// <param name="cipherText">the encrypted text to decrypt in base64 encoding</param>
		/// <param name="password">the password to use for encryption</param>
		/// <param name="salt">the salt to use for the encryption - if null or left out, no salting is done</param>
		/// <returns></returns>
		public string Decrypt( string cipherText, string password, string salt = null )
		{
			return SimpleDecryptWithPassword( cipherText, password );
		}

		/// <summary>
		/// Symmetrically encrypts the passed string using DPAPI and returns a base64 representation of the resulting cyphertext.
		/// This does not need a password, but the encrypted data can only be decrypted later on this computer by this same user.
		/// </summary>
		/// <param name="plainText">the text to encrypt</param>
		/// <returns></returns>
		public string EncryptLocal( string plainText )
		{
			return DpapiEncrypt( plainText );
		}

		/// <summary>
		/// Decrypts the passed cyphertext (as encrypted by the EncryptLocal method - symmetrically on this computer 
		/// by this same user and encoded in base64)
		/// and returns the resulting plaintext.
		/// </summary>
		/// <param name="cipherText">the encrypted text to decrypt in base64 encoding</param>
		/// <returns></returns>
		public string DecryptLocal( string cipherText )
		{
			return DpapiDecrypt( cipherText );
		}

		#region Implementation

		// From http://codereview.stackexchange.com/q/14892

		private static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create();

		//Preconfigured Encryption Parameters
		private static readonly int BlockBitSize = 128;
		private static readonly int KeyBitSize = 256;

		//Preconfigured Password Key Derivation Parameters
		private static readonly int SaltBitSize = 64;
		private static readonly int Iterations = 10000;
		private static readonly int MinPasswordLength = 12;

		/// <summary>
		/// Helper that generates a random key on each call.
		/// </summary>
		/// <returns></returns>
		private byte[] NewKey()
		{
			var key = new byte[KeyBitSize / 8];
			Random.GetBytes( key );
			return key;
		}

		/// <summary>
		/// Simple Encryption (AES) then Authentication (HMAC) for a UTF8 Message.
		/// </summary>
		/// <param name="secretMessage">The secret message.</param>
		/// <param name="cryptKey">The crypt key.</param>
		/// <param name="authKey">The auth key.</param>
		/// <param name="nonSecretPayload">(Optional) Non-Secret Payload.</param>
		/// <returns>
		/// Encrypted Message
		/// </returns>
		/// <exception cref="System.ArgumentException">Secret Message Required!;secretMessage</exception>
		/// <remarks>
		/// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize +  HMac-Tag(32)) * 1.33 Base64
		/// </remarks>
		private string SimpleEncrypt( string secretMessage, byte[] cryptKey, byte[] authKey,
						   byte[] nonSecretPayload = null )
		{
			if ( string.IsNullOrEmpty( secretMessage ) )
				throw new ArgumentException( "Secret Message Required!", "secretMessage" );

			var plainText = Encoding.UTF8.GetBytes( secretMessage );
			var cipherText = SimpleEncrypt( plainText, cryptKey, authKey, nonSecretPayload );
			return Convert.ToBase64String( cipherText );
		}

		/// <summary>
		/// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message.
		/// </summary>
		/// <param name="encryptedMessage">The encrypted message.</param>
		/// <param name="cryptKey">The crypt key.</param>
		/// <param name="authKey">The auth key.</param>
		/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
		/// <returns>
		/// Decrypted Message
		/// </returns>
		/// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
		private string SimpleDecrypt( string encryptedMessage, byte[] cryptKey, byte[] authKey,
						   int nonSecretPayloadLength = 0 )
		{
			if ( string.IsNullOrWhiteSpace( encryptedMessage ) )
				throw new ArgumentException( "Encrypted Message Required!", "encryptedMessage" );

			var cipherText = Convert.FromBase64String( encryptedMessage );
			var plainText = SimpleDecrypt( cipherText, cryptKey, authKey, nonSecretPayloadLength );
			return Encoding.UTF8.GetString( plainText );
		}

		/// <summary>
		/// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message
		/// using Keys derived from a Password (PBKDF2).
		/// </summary>
		/// <param name="secretMessage">The secret message.</param>
		/// <param name="password">The password.</param>
		/// <param name="nonSecretPayload">The non secret payload.</param>
		/// <returns>
		/// Encrypted Message
		/// </returns>
		/// <exception cref="System.ArgumentException">password</exception>
		/// <remarks>
		/// Significantly less secure than using random binary keys.
		/// Adds additional non secret payload for key generation parameters.
		/// </remarks>
		private string SimpleEncryptWithPassword( string secretMessage, string password,
								 byte[] nonSecretPayload = null )
		{
			if ( string.IsNullOrEmpty( secretMessage ) )
				throw new ArgumentException( "Secret Message Required!", "secretMessage" );

			var plainText = Encoding.UTF8.GetBytes( secretMessage );
			var cipherText = SimpleEncryptWithPassword( plainText, password, nonSecretPayload );
			return Convert.ToBase64String( cipherText );
		}

		/// <summary>
		/// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message
		/// using keys derived from a password (PBKDF2). 
		/// </summary>
		/// <param name="encryptedMessage">The encrypted message.</param>
		/// <param name="password">The password.</param>
		/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
		/// <returns>
		/// Decrypted Message
		/// </returns>
		/// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
		/// <remarks>
		/// Significantly less secure than using random binary keys.
		/// </remarks>
		private string SimpleDecryptWithPassword( string encryptedMessage, string password,
								 int nonSecretPayloadLength = 0 )
		{
			if ( string.IsNullOrWhiteSpace( encryptedMessage ) )
				throw new ArgumentException( "Encrypted Message Required!", "encryptedMessage" );

			var cipherText = Convert.FromBase64String( encryptedMessage );
			var plainText = SimpleDecryptWithPassword( cipherText, password, nonSecretPayloadLength );
			return Encoding.UTF8.GetString( plainText );
		}

		private byte[] SimpleEncrypt( byte[] secretMessage, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null )
		{
			//User Error Checks
			if ( cryptKey == null || cryptKey.Length != KeyBitSize / 8 )
				throw new ArgumentException( String.Format( "Key needs to be {0} bit!", KeyBitSize ), "cryptKey" );

			if ( authKey == null || authKey.Length != KeyBitSize / 8 )
				throw new ArgumentException( String.Format( "Key needs to be {0} bit!", KeyBitSize ), "authKey" );

			if ( secretMessage == null || secretMessage.Length < 1 )
				throw new ArgumentException( "Secret Message Required!", "secretMessage" );

			//non-secret payload optional
			nonSecretPayload = nonSecretPayload ?? new byte[] { };

			byte[] cipherText;
			byte[] iv;

			using ( var aes = new AesManaged
			{
				KeySize = KeyBitSize,
				BlockSize = BlockBitSize,
				Mode = CipherMode.CBC,
				Padding = PaddingMode.PKCS7
			} )
			{

				//Use random IV
				aes.GenerateIV();
				iv = aes.IV;

				using ( var encrypter = aes.CreateEncryptor( cryptKey, iv ) )
				using ( var cipherStream = new MemoryStream() )
				{
					using ( var cryptoStream = new CryptoStream( cipherStream, encrypter, CryptoStreamMode.Write ) )
					using ( var binaryWriter = new BinaryWriter( cryptoStream ) )
					{
						//Encrypt Data
						binaryWriter.Write( secretMessage );
					}

					cipherText = cipherStream.ToArray();
				}

			}

			//Assemble encrypted message and add authentication
			using ( var hmac = new HMACSHA256( authKey ) )
			using ( var encryptedStream = new MemoryStream() )
			{
				using ( var binaryWriter = new BinaryWriter( encryptedStream ) )
				{
					//Prepend non-secret payload if any
					binaryWriter.Write( nonSecretPayload );
					//Prepend IV
					binaryWriter.Write( iv );
					//Write Ciphertext
					binaryWriter.Write( cipherText );
					binaryWriter.Flush();

					//Authenticate all data
					var tag = hmac.ComputeHash( encryptedStream.ToArray() );
					//Postpend tag
					binaryWriter.Write( tag );
				}
				return encryptedStream.ToArray();
			}

		}

		private byte[] SimpleDecrypt( byte[] encryptedMessage, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0 )
		{

			//Basic Usage Error Checks
			if ( cryptKey == null || cryptKey.Length != KeyBitSize / 8 )
				throw new ArgumentException( String.Format( "CryptKey needs to be {0} bit!", KeyBitSize ), "cryptKey" );

			if ( authKey == null || authKey.Length != KeyBitSize / 8 )
				throw new ArgumentException( String.Format( "AuthKey needs to be {0} bit!", KeyBitSize ), "authKey" );

			if ( encryptedMessage == null || encryptedMessage.Length == 0 )
				throw new ArgumentException( "Encrypted Message Required!", "encryptedMessage" );

			using ( var hmac = new HMACSHA256( authKey ) )
			{
				var sentTag = new byte[hmac.HashSize / 8];
				//Calculate Tag
				var calcTag = hmac.ComputeHash( encryptedMessage, 0, encryptedMessage.Length - sentTag.Length );
				var ivLength = (BlockBitSize / 8);

				//if message length is to small just return null
				if ( encryptedMessage.Length < sentTag.Length + nonSecretPayloadLength + ivLength )
					return null;

				//Grab Sent Tag
				Array.Copy( encryptedMessage, encryptedMessage.Length - sentTag.Length, sentTag, 0, sentTag.Length );

				//Compare Tag with constant time comparison
				var compare = 0;
				for ( var i = 0; i < sentTag.Length; i++ )
					compare |= sentTag[i] ^ calcTag[i];

				//if message doesn't authenticate return null
				if ( compare != 0 )
					return null;

				using ( var aes = new AesManaged
				{
					KeySize = KeyBitSize,
					BlockSize = BlockBitSize,
					Mode = CipherMode.CBC,
					Padding = PaddingMode.PKCS7
				} )
				{

					//Grab IV from message
					var iv = new byte[ivLength];
					Array.Copy( encryptedMessage, nonSecretPayloadLength, iv, 0, iv.Length );

					using ( var decrypter = aes.CreateDecryptor( cryptKey, iv ) )
					using ( var plainTextStream = new MemoryStream() )
					{
						using ( var decrypterStream = new CryptoStream( plainTextStream, decrypter, CryptoStreamMode.Write ) )
						using ( var binaryWriter = new BinaryWriter( decrypterStream ) )
						{
							//Decrypt Cipher Text from Message
							binaryWriter.Write(
							  encryptedMessage,
							  nonSecretPayloadLength + iv.Length,
							  encryptedMessage.Length - nonSecretPayloadLength - iv.Length - sentTag.Length
							);
						}
						//Return Plain Text
						return plainTextStream.ToArray();
					}
				}
			}
		}

		private byte[] SimpleEncryptWithPassword( byte[] secretMessage, string password, byte[] nonSecretPayload = null )
		{
			nonSecretPayload = nonSecretPayload ?? new byte[] { };

			//User Error Checks
			if ( string.IsNullOrWhiteSpace( password ) || password.Length < MinPasswordLength )
				throw new ArgumentException( String.Format( "Must have a password of at least {0} characters!", MinPasswordLength ), "password" );

			if ( secretMessage == null || secretMessage.Length == 0 )
				throw new ArgumentException( "Secret Message Required!", "secretMessage" );

			var payload = new byte[((SaltBitSize / 8) * 2) + nonSecretPayload.Length];

			Array.Copy( nonSecretPayload, payload, nonSecretPayload.Length );
			int payloadIndex = nonSecretPayload.Length;

			byte[] cryptKey;
			byte[] authKey;
			//Use Random Salt to prevent pre-generated weak password attacks.
			using ( var generator = new Rfc2898DeriveBytes( password, SaltBitSize / 8, Iterations ) )
			{
				var salt = generator.Salt;

				//Generate Keys
				cryptKey = generator.GetBytes( KeyBitSize / 8 );

				//Create Non Secret Payload
				Array.Copy( salt, 0, payload, payloadIndex, salt.Length );
				payloadIndex += salt.Length;
			}

			//Deriving separate key, might be less efficient than using HKDF, 
			//but now compatible with RNEncryptor which had a very similar wireformat and requires less code than HKDF.
			using ( var generator = new Rfc2898DeriveBytes( password, SaltBitSize / 8, Iterations ) )
			{
				var salt = generator.Salt;

				//Generate Keys
				authKey = generator.GetBytes( KeyBitSize / 8 );

				//Create Rest of Non Secret Payload
				Array.Copy( salt, 0, payload, payloadIndex, salt.Length );
			}

			return SimpleEncrypt( secretMessage, cryptKey, authKey, payload );
		}

		private byte[] SimpleDecryptWithPassword( byte[] encryptedMessage, string password, int nonSecretPayloadLength = 0 )
		{
			//User Error Checks
			if ( string.IsNullOrWhiteSpace( password ) || password.Length < MinPasswordLength )
				throw new ArgumentException( String.Format( "Must have a password of at least {0} characters!", MinPasswordLength ), "password" );

			if ( encryptedMessage == null || encryptedMessage.Length == 0 )
				throw new ArgumentException( "Encrypted Message Required!", "encryptedMessage" );

			var cryptSalt = new byte[SaltBitSize / 8];
			var authSalt = new byte[SaltBitSize / 8];

			//Grab Salt from Non-Secret Payload
			Array.Copy( encryptedMessage, nonSecretPayloadLength, cryptSalt, 0, cryptSalt.Length );
			Array.Copy( encryptedMessage, nonSecretPayloadLength + cryptSalt.Length, authSalt, 0, authSalt.Length );

			byte[] cryptKey;
			byte[] authKey;

			//Generate crypt key
			using ( var generator = new Rfc2898DeriveBytes( password, cryptSalt, Iterations ) )
			{
				cryptKey = generator.GetBytes( KeyBitSize / 8 );
			}
			//Generate auth key
			using ( var generator = new Rfc2898DeriveBytes( password, authSalt, Iterations ) )
			{
				authKey = generator.GetBytes( KeyBitSize / 8 );
			}

			return SimpleDecrypt( encryptedMessage, cryptKey, authKey, cryptSalt.Length + authSalt.Length + nonSecretPayloadLength );
		}

		// From http://stackoverflow.com/a/1678609/301134

		/// <summary>
		/// Encrypts a given password and returns the encrypted data
		/// as a base64 string.
		/// </summary>
		/// <param name="plainText">An unencrypted string that needs
		/// to be secured.</param>
		/// <returns>A base64 encoded string that represents the encrypted
		/// binary data.
		/// </returns>
		/// <remarks>This solution is not really secure as we are
		/// keeping strings in memory. If runtime protection is essential,
		/// SecureString should be used.</remarks>
		/// <exception cref="ArgumentNullException">If <paramref name="plainText"/>
		/// is a null reference.</exception>
		private string DpapiEncrypt( string plainText )
		{
			if ( plainText == null )
				throw new ArgumentNullException( "plainText" );

			//encrypt data
			var data = Encoding.Unicode.GetBytes( plainText );
			byte[] encrypted = ProtectedData.Protect( data, null, DataProtectionScope.CurrentUser );

			//return as base64 string
			return Convert.ToBase64String( encrypted );
		}

		/// <summary>
		/// Decrypts a given string.
		/// </summary>
		/// <param name="cipher">A base64 encoded string that was created
		/// through the Encrypt(string) or
		/// Encrypt(SecureString) extension methods.</param>
		/// <returns>The decrypted string.</returns>
		/// <remarks>Keep in mind that the decrypted string remains in memory
		/// and makes your application vulnerable per se. If runtime protection
		/// is essential, SecureString should be used.</remarks>
		/// <exception cref="ArgumentNullException">If <paramref name="cipher"/>
		/// is a null reference.</exception>
		private string DpapiDecrypt( string cipher )
		{
			if ( cipher == null )
				throw new ArgumentNullException( "cipher" );

			//parse base64 string
			byte[] data = Convert.FromBase64String( cipher );

			//decrypt data
			byte[] decrypted = ProtectedData.Unprotect( data, null, DataProtectionScope.CurrentUser );
			return Encoding.Unicode.GetString( decrypted );
		}

		#endregion Implementation
	}
}
